/*
 * Copyright (c) 2001, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.imageio.plugins.jpeg;

import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import javax.imageio.event.IIOReadProgressListener;

import java.awt.Graphics;
import java.awt.color.ICC_Profile;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
import java.awt.image.IndexColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;

/**
 * A JFIF (JPEG File Interchange Format) APP0 (Application-Specific)
 * marker segment.  Inner classes are included for JFXX extension
 * marker segments, for different varieties of thumbnails, and for
 * ICC Profile APP2 marker segments.  Any of these secondary types
 * that occur are kept as members of a single JFIFMarkerSegment object.
 */
class JFIFMarkerSegment extends MarkerSegment {

  int majorVersion;
  int minorVersion;
  int resUnits;
  int Xdensity;
  int Ydensity;
  int thumbWidth;
  int thumbHeight;
  JFIFThumbRGB thumb = null;  // If present
  ArrayList extSegments = new ArrayList();
  ICCMarkerSegment iccSegment = null; // optional ICC
  private static final int THUMB_JPEG = 0x10;
  private static final int THUMB_PALETTE = 0x11;
  private static final int THUMB_UNASSIGNED = 0x12;
  private static final int THUMB_RGB = 0x13;
  private static final int DATA_SIZE = 14;
  private static final int ID_SIZE = 5;
  private final int MAX_THUMB_WIDTH = 255;
  private final int MAX_THUMB_HEIGHT = 255;

  private final boolean debug = false;

  /**
   * Set to <code>true</code> when reading the chunks of an
   * ICC profile.  All chunks are consolidated to create a single
   * "segment" containing all the chunks.  This flag is a state
   * variable identifying whether to construct a new segment or
   * append to an old one.
   */
  private boolean inICC = false;

  /**
   * A placeholder for an ICC profile marker segment under
   * construction.  The segment is not added to the list
   * until all chunks have been read.
   */
  private ICCMarkerSegment tempICCSegment = null;


  /**
   * Default constructor.  Used to create a default JFIF header
   */
  JFIFMarkerSegment() {
    super(JPEG.APP0);
    majorVersion = 1;
    minorVersion = 2;
    resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO;
    Xdensity = 1;
    Ydensity = 1;
    thumbWidth = 0;
    thumbHeight = 0;
  }

  /**
   * Constructs a JFIF header by reading from a stream wrapped
   * in a JPEGBuffer.
   */
  JFIFMarkerSegment(JPEGBuffer buffer) throws IOException {
    super(buffer);
    buffer.bufPtr += ID_SIZE;  // skip the id, we already checked it

    majorVersion = buffer.buf[buffer.bufPtr++];
    minorVersion = buffer.buf[buffer.bufPtr++];
    resUnits = buffer.buf[buffer.bufPtr++];
    Xdensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
    Xdensity |= buffer.buf[buffer.bufPtr++] & 0xff;
    Ydensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
    Ydensity |= buffer.buf[buffer.bufPtr++] & 0xff;
    thumbWidth = buffer.buf[buffer.bufPtr++] & 0xff;
    thumbHeight = buffer.buf[buffer.bufPtr++] & 0xff;
    buffer.bufAvail -= DATA_SIZE;
    if (thumbWidth > 0) {
      thumb = new JFIFThumbRGB(buffer, thumbWidth, thumbHeight);
    }
  }

  /**
   * Constructs a JFIF header from a DOM Node.
   */
  JFIFMarkerSegment(Node node) throws IIOInvalidTreeException {
    this();
    updateFromNativeNode(node, true);
  }

  /**
   * Returns a deep-copy clone of this object.
   */
  protected Object clone() {
    JFIFMarkerSegment newGuy = (JFIFMarkerSegment) super.clone();
    if (!extSegments.isEmpty()) { // Clone the list with a deep copy
      newGuy.extSegments = new ArrayList();
      for (Iterator iter = extSegments.iterator(); iter.hasNext(); ) {
        JFIFExtensionMarkerSegment jfxx =
            (JFIFExtensionMarkerSegment) iter.next();
        newGuy.extSegments.add(jfxx.clone());
      }
    }
    if (iccSegment != null) {
      newGuy.iccSegment = (ICCMarkerSegment) iccSegment.clone();
    }
    return newGuy;
  }

  /**
   * Add an JFXX extension marker segment from the stream wrapped
   * in the JPEGBuffer to the list of extension segments.
   */
  void addJFXX(JPEGBuffer buffer, JPEGImageReader reader)
      throws IOException {
    extSegments.add(new JFIFExtensionMarkerSegment(buffer, reader));
  }

  /**
   * Adds an ICC Profile APP2 segment from the stream wrapped
   * in the JPEGBuffer.
   */
  void addICC(JPEGBuffer buffer) throws IOException {
    if (inICC == false) {
      if (iccSegment != null) {
        throw new IIOException
            ("> 1 ICC APP2 Marker Segment not supported");
      }
      tempICCSegment = new ICCMarkerSegment(buffer);
      if (inICC == false) { // Just one chunk
        iccSegment = tempICCSegment;
        tempICCSegment = null;
      }
    } else {
      if (tempICCSegment.addData(buffer) == true) {
        iccSegment = tempICCSegment;
        tempICCSegment = null;
      }
    }
  }

  /**
   * Add an ICC Profile APP2 segment by constructing it from
   * the given ICC_ColorSpace object.
   */
  void addICC(ICC_ColorSpace cs) throws IOException {
    if (iccSegment != null) {
      throw new IIOException
          ("> 1 ICC APP2 Marker Segment not supported");
    }
    iccSegment = new ICCMarkerSegment(cs);
  }

  /**
   * Returns a tree of DOM nodes representing this object and any
   * subordinate JFXX extension or ICC Profile segments.
   */
  IIOMetadataNode getNativeNode() {
    IIOMetadataNode node = new IIOMetadataNode("app0JFIF");
    node.setAttribute("majorVersion", Integer.toString(majorVersion));
    node.setAttribute("minorVersion", Integer.toString(minorVersion));
    node.setAttribute("resUnits", Integer.toString(resUnits));
    node.setAttribute("Xdensity", Integer.toString(Xdensity));
    node.setAttribute("Ydensity", Integer.toString(Ydensity));
    node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
    node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
    if (!extSegments.isEmpty()) {
      IIOMetadataNode JFXXnode = new IIOMetadataNode("JFXX");
      node.appendChild(JFXXnode);
      for (Iterator iter = extSegments.iterator(); iter.hasNext(); ) {
        JFIFExtensionMarkerSegment seg =
            (JFIFExtensionMarkerSegment) iter.next();
        JFXXnode.appendChild(seg.getNativeNode());
      }
    }
    if (iccSegment != null) {
      node.appendChild(iccSegment.getNativeNode());
    }

    return node;
  }

  /**
   * Updates the data in this object from the given DOM Node tree.
   * If fromScratch is true, this object is being constructed.
   * Otherwise an existing object is being modified.
   * Throws an IIOInvalidTreeException if the tree is invalid in
   * any way.
   */
  void updateFromNativeNode(Node node, boolean fromScratch)
      throws IIOInvalidTreeException {
    // none of the attributes are required
    NamedNodeMap attrs = node.getAttributes();
    if (attrs.getLength() > 0) {
      int value = getAttributeValue(node, attrs, "majorVersion",
          0, 255, false);
      majorVersion = (value != -1) ? value : majorVersion;
      value = getAttributeValue(node, attrs, "minorVersion",
          0, 255, false);
      minorVersion = (value != -1) ? value : minorVersion;
      value = getAttributeValue(node, attrs, "resUnits", 0, 2, false);
      resUnits = (value != -1) ? value : resUnits;
      value = getAttributeValue(node, attrs, "Xdensity", 1, 65535, false);
      Xdensity = (value != -1) ? value : Xdensity;
      value = getAttributeValue(node, attrs, "Ydensity", 1, 65535, false);
      Ydensity = (value != -1) ? value : Ydensity;
      value = getAttributeValue(node, attrs, "thumbWidth", 0, 255, false);
      thumbWidth = (value != -1) ? value : thumbWidth;
      value = getAttributeValue(node, attrs, "thumbHeight", 0, 255, false);
      thumbHeight = (value != -1) ? value : thumbHeight;
    }
    if (node.hasChildNodes()) {
      NodeList children = node.getChildNodes();
      int count = children.getLength();
      if (count > 2) {
        throw new IIOInvalidTreeException
            ("app0JFIF node cannot have > 2 children", node);
      }
      for (int i = 0; i < count; i++) {
        Node child = children.item(i);
        String name = child.getNodeName();
        if (name.equals("JFXX")) {
          if ((!extSegments.isEmpty()) && fromScratch) {
            throw new IIOInvalidTreeException
                ("app0JFIF node cannot have > 1 JFXX node", node);
          }
          NodeList exts = child.getChildNodes();
          int extCount = exts.getLength();
          for (int j = 0; j < extCount; j++) {
            Node ext = exts.item(j);
            extSegments.add(new JFIFExtensionMarkerSegment(ext));
          }
        }
        if (name.equals("app2ICC")) {
          if ((iccSegment != null) && fromScratch) {
            throw new IIOInvalidTreeException
                ("> 1 ICC APP2 Marker Segment not supported", node);
          }
          iccSegment = new ICCMarkerSegment(child);
        }
      }
    }
  }

  int getThumbnailWidth(int index) {
    if (thumb != null) {
      if (index == 0) {
        return thumb.getWidth();
      }
      index--;
    }
    JFIFExtensionMarkerSegment jfxx =
        (JFIFExtensionMarkerSegment) extSegments.get(index);
    return jfxx.thumb.getWidth();
  }

  int getThumbnailHeight(int index) {
    if (thumb != null) {
      if (index == 0) {
        return thumb.getHeight();
      }
      index--;
    }
    JFIFExtensionMarkerSegment jfxx =
        (JFIFExtensionMarkerSegment) extSegments.get(index);
    return jfxx.thumb.getHeight();
  }

  BufferedImage getThumbnail(ImageInputStream iis,
      int index,
      JPEGImageReader reader) throws IOException {
    reader.thumbnailStarted(index);
    BufferedImage ret = null;
    if ((thumb != null) && (index == 0)) {
      ret = thumb.getThumbnail(iis, reader);
    } else {
      if (thumb != null) {
        index--;
      }
      JFIFExtensionMarkerSegment jfxx =
          (JFIFExtensionMarkerSegment) extSegments.get(index);
      ret = jfxx.thumb.getThumbnail(iis, reader);
    }
    reader.thumbnailComplete();
    return ret;
  }


  /**
   * Writes the data for this segment to the stream in
   * valid JPEG format.  Assumes that there will be no thumbnail.
   */
  void write(ImageOutputStream ios,
      JPEGImageWriter writer) throws IOException {
    // No thumbnail
    write(ios, null, writer);
  }

  /**
   * Writes the data for this segment to the stream in
   * valid JPEG format.  The length written takes the thumbnail
   * width and height into account.  If necessary, the thumbnail
   * is clipped to 255 x 255 and a warning is sent to the writer
   * argument.  Progress updates are sent to the writer argument.
   */
  void write(ImageOutputStream ios,
      BufferedImage thumb,
      JPEGImageWriter writer) throws IOException {
    int thumbWidth = 0;
    int thumbHeight = 0;
    int thumbLength = 0;
    int[] thumbData = null;
    if (thumb != null) {
      // Clip if necessary and get the data in thumbData
      thumbWidth = thumb.getWidth();
      thumbHeight = thumb.getHeight();
      if ((thumbWidth > MAX_THUMB_WIDTH)
          || (thumbHeight > MAX_THUMB_HEIGHT)) {
        writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
      }
      thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
      thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
      thumbData = thumb.getRaster().getPixels(0, 0,
          thumbWidth, thumbHeight,
          (int[]) null);
      thumbLength = thumbData.length;
    }
    length = DATA_SIZE + LENGTH_SIZE + thumbLength;
    writeTag(ios);
    byte[] id = {0x4A, 0x46, 0x49, 0x46, 0x00};
    ios.write(id);
    ios.write(majorVersion);
    ios.write(minorVersion);
    ios.write(resUnits);
    write2bytes(ios, Xdensity);
    write2bytes(ios, Ydensity);
    ios.write(thumbWidth);
    ios.write(thumbHeight);
    if (thumbData != null) {
      writer.thumbnailStarted(0);
      writeThumbnailData(ios, thumbData, writer);
      writer.thumbnailComplete();
    }
  }

  /*
   * Write out the values in the integer array as a sequence of bytes,
   * reporting progress to the writer argument.
   */
  void writeThumbnailData(ImageOutputStream ios,
      int[] thumbData,
      JPEGImageWriter writer) throws IOException {
    int progInterval = thumbData.length / 20;  // approx. every 5%
    if (progInterval == 0) {
      progInterval = 1;
    }
    for (int i = 0; i < thumbData.length; i++) {
      ios.write(thumbData[i]);
      if ((i > progInterval) && (i % progInterval == 0)) {
        writer.thumbnailProgress
            (((float) i * 100) / ((float) thumbData.length));
      }
    }
  }

  /**
   * Write out this JFIF Marker Segment, including a thumbnail or
   * appending a series of JFXX Marker Segments, as appropriate.
   * Warnings and progress reports are sent to the writer argument.
   * The list of thumbnails is matched to the list of JFXX extension
   * segments, if any, in order to determine how to encode the
   * thumbnails.  If there are more thumbnails than metadata segments,
   * default encoding is used for the extra thumbnails.
   */
  void writeWithThumbs(ImageOutputStream ios,
      List thumbnails,
      JPEGImageWriter writer) throws IOException {
    if (thumbnails != null) {
      JFIFExtensionMarkerSegment jfxx = null;
      if (thumbnails.size() == 1) {
        if (!extSegments.isEmpty()) {
          jfxx = (JFIFExtensionMarkerSegment) extSegments.get(0);
        }
        writeThumb(ios,
            (BufferedImage) thumbnails.get(0),
            jfxx,
            0,
            true,
            writer);
      } else {
        // All others write as separate JFXX segments
        write(ios, writer);  // Just the header without any thumbnail
        for (int i = 0; i < thumbnails.size(); i++) {
          jfxx = null;
          if (i < extSegments.size()) {
            jfxx = (JFIFExtensionMarkerSegment) extSegments.get(i);
          }
          writeThumb(ios,
              (BufferedImage) thumbnails.get(i),
              jfxx,
              i,
              false,
              writer);
        }
      }
    } else {  // No thumbnails
      write(ios, writer);
    }

  }

  private void writeThumb(ImageOutputStream ios,
      BufferedImage thumb,
      JFIFExtensionMarkerSegment jfxx,
      int index,
      boolean onlyOne,
      JPEGImageWriter writer) throws IOException {
    ColorModel cm = thumb.getColorModel();
    ColorSpace cs = cm.getColorSpace();

    if (cm instanceof IndexColorModel) {
      // We never write a palette image into the header
      // So if it's the only one, we need to write the header first
      if (onlyOne) {
        write(ios, writer);
      }
      if ((jfxx == null)
          || (jfxx.code == THUMB_PALETTE)) {
        writeJFXXSegment(index, thumb, ios, writer); // default
      } else {
        // Expand to RGB
        BufferedImage thumbRGB =
            ((IndexColorModel) cm).convertToIntDiscrete
                (thumb.getRaster(), false);
        jfxx.setThumbnail(thumbRGB);
        writer.thumbnailStarted(index);
        jfxx.write(ios, writer);  // Handles clipping if needed
        writer.thumbnailComplete();
      }
    } else if (cs.getType() == ColorSpace.TYPE_RGB) {
      if (jfxx == null) {
        if (onlyOne) {
          write(ios, thumb, writer); // As part of the header
        } else {
          writeJFXXSegment(index, thumb, ios, writer); // default
        }
      } else {
        // If this is the only one, write the header first
        if (onlyOne) {
          write(ios, writer);
        }
        if (jfxx.code == THUMB_PALETTE) {
          writeJFXXSegment(index, thumb, ios, writer); // default
          writer.warningOccurred
              (JPEGImageWriter.WARNING_NO_RGB_THUMB_AS_INDEXED);
        } else {
          jfxx.setThumbnail(thumb);
          writer.thumbnailStarted(index);
          jfxx.write(ios, writer);  // Handles clipping if needed
          writer.thumbnailComplete();
        }
      }
    } else if (cs.getType() == ColorSpace.TYPE_GRAY) {
      if (jfxx == null) {
        if (onlyOne) {
          BufferedImage thumbRGB = expandGrayThumb(thumb);
          write(ios, thumbRGB, writer); // As part of the header
        } else {
          writeJFXXSegment(index, thumb, ios, writer); // default
        }
      } else {
        // If this is the only one, write the header first
        if (onlyOne) {
          write(ios, writer);
        }
        if (jfxx.code == THUMB_RGB) {
          BufferedImage thumbRGB = expandGrayThumb(thumb);
          writeJFXXSegment(index, thumbRGB, ios, writer);
        } else if (jfxx.code == THUMB_JPEG) {
          jfxx.setThumbnail(thumb);
          writer.thumbnailStarted(index);
          jfxx.write(ios, writer);  // Handles clipping if needed
          writer.thumbnailComplete();
        } else if (jfxx.code == THUMB_PALETTE) {
          writeJFXXSegment(index, thumb, ios, writer); // default
          writer.warningOccurred
              (JPEGImageWriter.WARNING_NO_GRAY_THUMB_AS_INDEXED);
        }
      }
    } else {
      writer.warningOccurred
          (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL);
    }
  }

  // Could put reason codes in here to be parsed in writeJFXXSegment
  // in order to provide more meaningful warnings.
  private class IllegalThumbException extends Exception {

  }

  /**
   * Writes out a new JFXX extension segment, without saving it.
   */
  private void writeJFXXSegment(int index,
      BufferedImage thumbnail,
      ImageOutputStream ios,
      JPEGImageWriter writer) throws IOException {
    JFIFExtensionMarkerSegment jfxx = null;
    try {
      jfxx = new JFIFExtensionMarkerSegment(thumbnail);
    } catch (IllegalThumbException e) {
      writer.warningOccurred
          (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL);
      return;
    }
    writer.thumbnailStarted(index);
    jfxx.write(ios, writer);
    writer.thumbnailComplete();
  }


  /**
   * Return an RGB image that is the expansion of the given grayscale
   * image.
   */
  private static BufferedImage expandGrayThumb(BufferedImage thumb) {
    BufferedImage ret = new BufferedImage(thumb.getWidth(),
        thumb.getHeight(),
        BufferedImage.TYPE_INT_RGB);
    Graphics g = ret.getGraphics();
    g.drawImage(thumb, 0, 0, null);
    return ret;
  }

  /**
   * Writes out a default JFIF marker segment to the given
   * output stream.  If <code>thumbnails</code> is not <code>null</code>,
   * writes out the set of thumbnail images as JFXX marker segments, or
   * incorporated into the JFIF segment if appropriate.
   * If <code>iccProfile</code> is not <code>null</code>,
   * writes out the profile after the JFIF segment using as many APP2
   * marker segments as necessary.
   */
  static void writeDefaultJFIF(ImageOutputStream ios,
      List thumbnails,
      ICC_Profile iccProfile,
      JPEGImageWriter writer)
      throws IOException {

    JFIFMarkerSegment jfif = new JFIFMarkerSegment();
    jfif.writeWithThumbs(ios, thumbnails, writer);
    if (iccProfile != null) {
      writeICC(iccProfile, ios);
    }
  }

  /**
   * Prints out the contents of this object to System.out for debugging.
   */
  void print() {
    printTag("JFIF");
    System.out.print("Version ");
    System.out.print(majorVersion);
    System.out.println(".0"
        + Integer.toString(minorVersion));
    System.out.print("Resolution units: ");
    System.out.println(resUnits);
    System.out.print("X density: ");
    System.out.println(Xdensity);
    System.out.print("Y density: ");
    System.out.println(Ydensity);
    System.out.print("Thumbnail Width: ");
    System.out.println(thumbWidth);
    System.out.print("Thumbnail Height: ");
    System.out.println(thumbHeight);
    if (!extSegments.isEmpty()) {
      for (Iterator iter = extSegments.iterator(); iter.hasNext(); ) {
        JFIFExtensionMarkerSegment extSegment =
            (JFIFExtensionMarkerSegment) iter.next();
        extSegment.print();
      }
    }
    if (iccSegment != null) {
      iccSegment.print();
    }
  }

  /**
   * A JFIF extension APP0 marker segment.
   */
  class JFIFExtensionMarkerSegment extends MarkerSegment {

    int code;
    JFIFThumb thumb;
    private static final int DATA_SIZE = 6;
    private static final int ID_SIZE = 5;

    JFIFExtensionMarkerSegment(JPEGBuffer buffer, JPEGImageReader reader)
        throws IOException {

      super(buffer);
      buffer.bufPtr += ID_SIZE;  // skip the id, we already checked it

      code = buffer.buf[buffer.bufPtr++] & 0xff;
      buffer.bufAvail -= DATA_SIZE;
      if (code == THUMB_JPEG) {
        thumb = new JFIFThumbJPEG(buffer, length, reader);
      } else {
        buffer.loadBuf(2);
        int thumbX = buffer.buf[buffer.bufPtr++] & 0xff;
        int thumbY = buffer.buf[buffer.bufPtr++] & 0xff;
        buffer.bufAvail -= 2;
        // following constructors handle bufAvail
        if (code == THUMB_PALETTE) {
          thumb = new JFIFThumbPalette(buffer, thumbX, thumbY);
        } else {
          thumb = new JFIFThumbRGB(buffer, thumbX, thumbY);
        }
      }
    }

    JFIFExtensionMarkerSegment(Node node) throws IIOInvalidTreeException {
      super(JPEG.APP0);
      NamedNodeMap attrs = node.getAttributes();
      if (attrs.getLength() > 0) {
        code = getAttributeValue(node,
            attrs,
            "extensionCode",
            THUMB_JPEG,
            THUMB_RGB,
            false);
        if (code == THUMB_UNASSIGNED) {
          throw new IIOInvalidTreeException
              ("invalid extensionCode attribute value", node);
        }
      } else {
        code = THUMB_UNASSIGNED;
      }
      // Now the child
      if (node.getChildNodes().getLength() != 1) {
        throw new IIOInvalidTreeException
            ("app0JFXX node must have exactly 1 child", node);
      }
      Node child = node.getFirstChild();
      String name = child.getNodeName();
      if (name.equals("JFIFthumbJPEG")) {
        if (code == THUMB_UNASSIGNED) {
          code = THUMB_JPEG;
        }
        thumb = new JFIFThumbJPEG(child);
      } else if (name.equals("JFIFthumbPalette")) {
        if (code == THUMB_UNASSIGNED) {
          code = THUMB_PALETTE;
        }
        thumb = new JFIFThumbPalette(child);
      } else if (name.equals("JFIFthumbRGB")) {
        if (code == THUMB_UNASSIGNED) {
          code = THUMB_RGB;
        }
        thumb = new JFIFThumbRGB(child);
      } else {
        throw new IIOInvalidTreeException
            ("unrecognized app0JFXX child node", node);
      }
    }

    JFIFExtensionMarkerSegment(BufferedImage thumbnail)
        throws IllegalThumbException {

      super(JPEG.APP0);
      ColorModel cm = thumbnail.getColorModel();
      int csType = cm.getColorSpace().getType();
      if (cm.hasAlpha()) {
        throw new IllegalThumbException();
      }
      if (cm instanceof IndexColorModel) {
        code = THUMB_PALETTE;
        thumb = new JFIFThumbPalette(thumbnail);
      } else if (csType == ColorSpace.TYPE_RGB) {
        code = THUMB_RGB;
        thumb = new JFIFThumbRGB(thumbnail);
      } else if (csType == ColorSpace.TYPE_GRAY) {
        code = THUMB_JPEG;
        thumb = new JFIFThumbJPEG(thumbnail);
      } else {
        throw new IllegalThumbException();
      }
    }

    void setThumbnail(BufferedImage thumbnail) {
      try {
        switch (code) {
          case THUMB_PALETTE:
            thumb = new JFIFThumbPalette(thumbnail);
            break;
          case THUMB_RGB:
            thumb = new JFIFThumbRGB(thumbnail);
            break;
          case THUMB_JPEG:
            thumb = new JFIFThumbJPEG(thumbnail);
            break;
        }
      } catch (IllegalThumbException e) {
        // Should never happen
        throw new InternalError("Illegal thumb in setThumbnail!", e);
      }
    }

    protected Object clone() {
      JFIFExtensionMarkerSegment newGuy =
          (JFIFExtensionMarkerSegment) super.clone();
      if (thumb != null) {
        newGuy.thumb = (JFIFThumb) thumb.clone();
      }
      return newGuy;
    }

    IIOMetadataNode getNativeNode() {
      IIOMetadataNode node = new IIOMetadataNode("app0JFXX");
      node.setAttribute("extensionCode", Integer.toString(code));
      node.appendChild(thumb.getNativeNode());
      return node;
    }

    void write(ImageOutputStream ios,
        JPEGImageWriter writer) throws IOException {
      length = LENGTH_SIZE + DATA_SIZE + thumb.getLength();
      writeTag(ios);
      byte[] id = {0x4A, 0x46, 0x58, 0x58, 0x00};
      ios.write(id);
      ios.write(code);
      thumb.write(ios, writer);
    }

    void print() {
      printTag("JFXX");
      thumb.print();
    }
  }

  /**
   * A superclass for the varieties of thumbnails that can
   * be stored in a JFIF extension marker segment.
   */
  abstract class JFIFThumb implements Cloneable {

    long streamPos = -1L;  // Save the thumbnail pos when reading

    abstract int getLength(); // When writing

    abstract int getWidth();

    abstract int getHeight();

    abstract BufferedImage getThumbnail(ImageInputStream iis,
        JPEGImageReader reader)
        throws IOException;

    protected JFIFThumb() {
    }

    protected JFIFThumb(JPEGBuffer buffer) throws IOException {
      // Save the stream position for reading the thumbnail later
      streamPos = buffer.getStreamPosition();
    }

    abstract void print();

    abstract IIOMetadataNode getNativeNode();

    abstract void write(ImageOutputStream ios,
        JPEGImageWriter writer) throws IOException;

    protected Object clone() {
      try {
        return super.clone();
      } catch (CloneNotSupportedException e) {
      } // won't happen
      return null;
    }

  }

  abstract class JFIFThumbUncompressed extends JFIFThumb {

    BufferedImage thumbnail = null;
    int thumbWidth;
    int thumbHeight;
    String name;

    JFIFThumbUncompressed(JPEGBuffer buffer,
        int width,
        int height,
        int skip,
        String name)
        throws IOException {
      super(buffer);
      thumbWidth = width;
      thumbHeight = height;
      // Now skip the thumbnail data
      buffer.skipData(skip);
      this.name = name;
    }

    JFIFThumbUncompressed(Node node, String name)
        throws IIOInvalidTreeException {

      thumbWidth = 0;
      thumbHeight = 0;
      this.name = name;
      NamedNodeMap attrs = node.getAttributes();
      int count = attrs.getLength();
      if (count > 2) {
        throw new IIOInvalidTreeException
            (name + " node cannot have > 2 attributes", node);
      }
      if (count != 0) {
        int value = getAttributeValue(node, attrs, "thumbWidth",
            0, 255, false);
        thumbWidth = (value != -1) ? value : thumbWidth;
        value = getAttributeValue(node, attrs, "thumbHeight",
            0, 255, false);
        thumbHeight = (value != -1) ? value : thumbHeight;
      }
    }

    JFIFThumbUncompressed(BufferedImage thumb) {
      thumbnail = thumb;
      thumbWidth = thumb.getWidth();
      thumbHeight = thumb.getHeight();
      name = null;  // not used when writing
    }

    void readByteBuffer(ImageInputStream iis,
        byte[] data,
        JPEGImageReader reader,
        float workPortion,
        float workOffset) throws IOException {
      int progInterval = Math.max((int) (data.length / 20 / workPortion),
          1);
      for (int offset = 0;
          offset < data.length; ) {
        int len = Math.min(progInterval, data.length - offset);
        iis.read(data, offset, len);
        offset += progInterval;
        float percentDone = ((float) offset * 100)
            / data.length
            * workPortion + workOffset;
        if (percentDone > 100.0F) {
          percentDone = 100.0F;
        }
        reader.thumbnailProgress(percentDone);
      }
    }


    int getWidth() {
      return thumbWidth;
    }

    int getHeight() {
      return thumbHeight;
    }

    IIOMetadataNode getNativeNode() {
      IIOMetadataNode node = new IIOMetadataNode(name);
      node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
      node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
      return node;
    }

    void write(ImageOutputStream ios,
        JPEGImageWriter writer) throws IOException {
      if ((thumbWidth > MAX_THUMB_WIDTH)
          || (thumbHeight > MAX_THUMB_HEIGHT)) {
        writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
      }
      thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
      thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
      ios.write(thumbWidth);
      ios.write(thumbHeight);
    }

    void writePixels(ImageOutputStream ios,
        JPEGImageWriter writer) throws IOException {
      if ((thumbWidth > MAX_THUMB_WIDTH)
          || (thumbHeight > MAX_THUMB_HEIGHT)) {
        writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
      }
      thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
      thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
      int[] data = thumbnail.getRaster().getPixels(0, 0,
          thumbWidth,
          thumbHeight,
          (int[]) null);
      writeThumbnailData(ios, data, writer);
    }

    void print() {
      System.out.print(name + " width: ");
      System.out.println(thumbWidth);
      System.out.print(name + " height: ");
      System.out.println(thumbHeight);
    }

  }

  /**
   * A JFIF thumbnail stored as RGB, one byte per channel,
   * interleaved.
   */
  class JFIFThumbRGB extends JFIFThumbUncompressed {

    JFIFThumbRGB(JPEGBuffer buffer, int width, int height)
        throws IOException {

      super(buffer, width, height, width * height * 3, "JFIFthumbRGB");
    }

    JFIFThumbRGB(Node node) throws IIOInvalidTreeException {
      super(node, "JFIFthumbRGB");
    }

    JFIFThumbRGB(BufferedImage thumb) throws IllegalThumbException {
      super(thumb);
    }

    int getLength() {
      return (thumbWidth * thumbHeight * 3);
    }

    BufferedImage getThumbnail(ImageInputStream iis,
        JPEGImageReader reader)
        throws IOException {
      iis.mark();
      iis.seek(streamPos);
      DataBufferByte buffer = new DataBufferByte(getLength());
      readByteBuffer(iis,
          buffer.getData(),
          reader,
          1.0F,
          0.0F);
      iis.reset();

      WritableRaster raster =
          Raster.createInterleavedRaster(buffer,
              thumbWidth,
              thumbHeight,
              thumbWidth * 3,
              3,
              new int[]{0, 1, 2},
              null);
      ColorModel cm = new ComponentColorModel(JPEG.JCS.sRGB,
          false,
          false,
          ColorModel.OPAQUE,
          DataBuffer.TYPE_BYTE);
      return new BufferedImage(cm,
          raster,
          false,
          null);
    }

    void write(ImageOutputStream ios,
        JPEGImageWriter writer) throws IOException {
      super.write(ios, writer); // width and height
      writePixels(ios, writer);
    }

  }

  /**
   * A JFIF thumbnail stored as an indexed palette image
   * using an RGB palette.
   */
  class JFIFThumbPalette extends JFIFThumbUncompressed {

    private static final int PALETTE_SIZE = 768;

    JFIFThumbPalette(JPEGBuffer buffer, int width, int height)
        throws IOException {
      super(buffer,
          width,
          height,
          PALETTE_SIZE + width * height,
          "JFIFThumbPalette");
    }

    JFIFThumbPalette(Node node) throws IIOInvalidTreeException {
      super(node, "JFIFThumbPalette");
    }

    JFIFThumbPalette(BufferedImage thumb) throws IllegalThumbException {
      super(thumb);
      IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
      if (icm.getMapSize() > 256) {
        throw new IllegalThumbException();
      }
    }

    int getLength() {
      return (thumbWidth * thumbHeight + PALETTE_SIZE);
    }

    BufferedImage getThumbnail(ImageInputStream iis,
        JPEGImageReader reader)
        throws IOException {
      iis.mark();
      iis.seek(streamPos);
      // read the palette
      byte[] palette = new byte[PALETTE_SIZE];
      float palettePart = ((float) PALETTE_SIZE) / getLength();
      readByteBuffer(iis,
          palette,
          reader,
          palettePart,
          0.0F);
      DataBufferByte buffer = new DataBufferByte(thumbWidth * thumbHeight);
      readByteBuffer(iis,
          buffer.getData(),
          reader,
          1.0F - palettePart,
          palettePart);
      iis.read();
      iis.reset();

      IndexColorModel cm = new IndexColorModel(8,
          256,
          palette,
          0,
          false);
      SampleModel sm = cm.createCompatibleSampleModel(thumbWidth,
          thumbHeight);
      WritableRaster raster =
          Raster.createWritableRaster(sm, buffer, null);
      return new BufferedImage(cm,
          raster,
          false,
          null);
    }

    void write(ImageOutputStream ios,
        JPEGImageWriter writer) throws IOException {
      super.write(ios, writer); // width and height
      // Write the palette (must be 768 bytes)
      byte[] palette = new byte[768];
      IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
      byte[] reds = new byte[256];
      byte[] greens = new byte[256];
      byte[] blues = new byte[256];
      icm.getReds(reds);
      icm.getGreens(greens);
      icm.getBlues(blues);
      for (int i = 0; i < 256; i++) {
        palette[i * 3] = reds[i];
        palette[i * 3 + 1] = greens[i];
        palette[i * 3 + 2] = blues[i];
      }
      ios.write(palette);
      writePixels(ios, writer);
    }
  }


  /**
   * A JFIF thumbnail stored as a JPEG stream.  No JFIF or
   * JFIF extension markers are permitted.  There is no need
   * to clip these, but the entire image must fit into a
   * single JFXX marker segment.
   */
  class JFIFThumbJPEG extends JFIFThumb {

    JPEGMetadata thumbMetadata = null;
    byte[] data = null;  // Compressed image data, for writing
    private static final int PREAMBLE_SIZE = 6;

    JFIFThumbJPEG(JPEGBuffer buffer,
        int length,
        JPEGImageReader reader) throws IOException {
      super(buffer);
      // Compute the final stream position
      long finalPos = streamPos + (length - PREAMBLE_SIZE);
      // Set the stream back to the start of the thumbnail
      // and read its metadata (but don't decode the image)
      buffer.iis.seek(streamPos);
      thumbMetadata = new JPEGMetadata(false, true, buffer.iis, reader);
      // Set the stream to the computed final position
      buffer.iis.seek(finalPos);
      // Clear the now invalid buffer
      buffer.bufAvail = 0;
      buffer.bufPtr = 0;
    }

    JFIFThumbJPEG(Node node) throws IIOInvalidTreeException {
      if (node.getChildNodes().getLength() > 1) {
        throw new IIOInvalidTreeException
            ("JFIFThumbJPEG node must have 0 or 1 child", node);
      }
      Node child = node.getFirstChild();
      if (child != null) {
        String name = child.getNodeName();
        if (!name.equals("markerSequence")) {
          throw new IIOInvalidTreeException
              ("JFIFThumbJPEG child must be a markerSequence node",
                  node);
        }
        thumbMetadata = new JPEGMetadata(false, true);
        thumbMetadata.setFromMarkerSequenceNode(child);
      }
    }

    JFIFThumbJPEG(BufferedImage thumb) throws IllegalThumbException {
      int INITIAL_BUFSIZE = 4096;
      int MAZ_BUFSIZE = 65535 - 2 - PREAMBLE_SIZE;
      try {
        ByteArrayOutputStream baos =
            new ByteArrayOutputStream(INITIAL_BUFSIZE);
        MemoryCacheImageOutputStream mos =
            new MemoryCacheImageOutputStream(baos);

        JPEGImageWriter thumbWriter = new JPEGImageWriter(null);

        thumbWriter.setOutput(mos);

        // get default metadata for the thumb
        JPEGMetadata metadata =
            (JPEGMetadata) thumbWriter.getDefaultImageMetadata
                (new ImageTypeSpecifier(thumb), null);

        // Remove the jfif segment, which should be there.
        MarkerSegment jfif = metadata.findMarkerSegment
            (JFIFMarkerSegment.class, true);
        if (jfif == null) {
          throw new IllegalThumbException();
        }

        metadata.markerSequence.remove(jfif);

                /*  Use this if removing leaves a hole and causes trouble

                // Get the tree
                String format = metadata.getNativeMetadataFormatName();
                IIOMetadataNode tree =
                (IIOMetadataNode) metadata.getAsTree(format);

                // If there is no app0jfif node, the image is bad
                NodeList jfifs = tree.getElementsByTagName("app0JFIF");
                if (jfifs.getLength() == 0) {
                throw new IllegalThumbException();
                }

                // remove the app0jfif node
                Node jfif = jfifs.item(0);
                Node parent = jfif.getParentNode();
                parent.removeChild(jfif);

                metadata.setFromTree(format, tree);
                */

        thumbWriter.write(new IIOImage(thumb, null, metadata));

        thumbWriter.dispose();
        // Now check that the size is OK
        if (baos.size() > MAZ_BUFSIZE) {
          throw new IllegalThumbException();
        }
        data = baos.toByteArray();
      } catch (IOException e) {
        throw new IllegalThumbException();
      }
    }

    int getWidth() {
      int retval = 0;
      SOFMarkerSegment sof =
          (SOFMarkerSegment) thumbMetadata.findMarkerSegment
              (SOFMarkerSegment.class, true);
      if (sof != null) {
        retval = sof.samplesPerLine;
      }
      return retval;
    }

    int getHeight() {
      int retval = 0;
      SOFMarkerSegment sof =
          (SOFMarkerSegment) thumbMetadata.findMarkerSegment
              (SOFMarkerSegment.class, true);
      if (sof != null) {
        retval = sof.numLines;
      }
      return retval;
    }

    private class ThumbnailReadListener
        implements IIOReadProgressListener {

      JPEGImageReader reader = null;

      ThumbnailReadListener(JPEGImageReader reader) {
        this.reader = reader;
      }

      public void sequenceStarted(ImageReader source, int minIndex) {
      }

      public void sequenceComplete(ImageReader source) {
      }

      public void imageStarted(ImageReader source, int imageIndex) {
      }

      public void imageProgress(ImageReader source,
          float percentageDone) {
        reader.thumbnailProgress(percentageDone);
      }

      public void imageComplete(ImageReader source) {
      }

      public void thumbnailStarted(ImageReader source,
          int imageIndex, int thumbnailIndex) {
      }

      public void thumbnailProgress(ImageReader source, float percentageDone) {
      }

      public void thumbnailComplete(ImageReader source) {
      }

      public void readAborted(ImageReader source) {
      }
    }

    BufferedImage getThumbnail(ImageInputStream iis,
        JPEGImageReader reader)
        throws IOException {
      iis.mark();
      iis.seek(streamPos);
      JPEGImageReader thumbReader = new JPEGImageReader(null);
      thumbReader.setInput(iis);
      thumbReader.addIIOReadProgressListener
          (new ThumbnailReadListener(reader));
      BufferedImage ret = thumbReader.read(0, null);
      thumbReader.dispose();
      iis.reset();
      return ret;
    }

    protected Object clone() {
      JFIFThumbJPEG newGuy = (JFIFThumbJPEG) super.clone();
      if (thumbMetadata != null) {
        newGuy.thumbMetadata = (JPEGMetadata) thumbMetadata.clone();
      }
      return newGuy;
    }

    IIOMetadataNode getNativeNode() {
      IIOMetadataNode node = new IIOMetadataNode("JFIFthumbJPEG");
      if (thumbMetadata != null) {
        node.appendChild(thumbMetadata.getNativeTree());
      }
      return node;
    }

    int getLength() {
      if (data == null) {
        return 0;
      } else {
        return data.length;
      }
    }

    void write(ImageOutputStream ios,
        JPEGImageWriter writer) throws IOException {
      int progInterval = data.length / 20;  // approx. every 5%
      if (progInterval == 0) {
        progInterval = 1;
      }
      for (int offset = 0;
          offset < data.length; ) {
        int len = Math.min(progInterval, data.length - offset);
        ios.write(data, offset, len);
        offset += progInterval;
        float percentDone = ((float) offset * 100) / data.length;
        if (percentDone > 100.0F) {
          percentDone = 100.0F;
        }
        writer.thumbnailProgress(percentDone);
      }
    }

    void print() {
      System.out.println("JFIF thumbnail stored as JPEG");
    }
  }

  /**
   * Write out the given profile to the stream, embedded in
   * the necessary number of APP2 segments, per the ICC spec.
   * This is the only mechanism for writing an ICC profile
   * to a stream.
   */
  static void writeICC(ICC_Profile profile, ImageOutputStream ios)
      throws IOException {
    int LENGTH_LENGTH = 2;
    final String ID = "ICC_PROFILE";
    int ID_LENGTH = ID.length() + 1; // spec says it's null-terminated
    int COUNTS_LENGTH = 2;
    int MAX_ICC_CHUNK_SIZE =
        65535 - LENGTH_LENGTH - ID_LENGTH - COUNTS_LENGTH;

    byte[] data = profile.getData();
    int numChunks = data.length / MAX_ICC_CHUNK_SIZE;
    if ((data.length % MAX_ICC_CHUNK_SIZE) != 0) {
      numChunks++;
    }
    int chunkNum = 1;
    int offset = 0;
    for (int i = 0; i < numChunks; i++) {
      int dataLength = Math.min(data.length - offset, MAX_ICC_CHUNK_SIZE);
      int segLength = dataLength + COUNTS_LENGTH + ID_LENGTH + LENGTH_LENGTH;
      ios.write(0xff);
      ios.write(JPEG.APP2);
      MarkerSegment.write2bytes(ios, segLength);
      byte[] id = ID.getBytes("US-ASCII");
      ios.write(id);
      ios.write(0); // Null-terminate the string
      ios.write(chunkNum++);
      ios.write(numChunks);
      ios.write(data, offset, dataLength);
      offset += dataLength;
    }
  }

  /**
   * An APP2 marker segment containing an ICC profile.  In the stream
   * a profile larger than 64K is broken up into a series of chunks.
   * This inner class represents the complete profile as a single object,
   * combining chunks as necessary.
   */
  class ICCMarkerSegment extends MarkerSegment {

    ArrayList chunks = null;
    byte[] profile = null; // The complete profile when it's fully read
    // May remain null when writing
    private static final int ID_SIZE = 12;
    int chunksRead;
    int numChunks;

    ICCMarkerSegment(ICC_ColorSpace cs) {
      super(JPEG.APP2);
      chunks = null;
      chunksRead = 0;
      numChunks = 0;
      profile = cs.getProfile().getData();
    }

    ICCMarkerSegment(JPEGBuffer buffer) throws IOException {
      super(buffer);  // gets whole segment or fills the buffer
      if (debug) {
        System.out.println("Creating new ICC segment");
      }
      buffer.bufPtr += ID_SIZE; // Skip the id
      buffer.bufAvail -= ID_SIZE;
            /*
             * Reduce the stored length by the id size.  The stored
             * length is used to store the length of the profile
             * data only.
             */
      length -= ID_SIZE;

      // get the chunk number
      int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
      // get the total number of chunks
      numChunks = buffer.buf[buffer.bufPtr + 1] & 0xff;

      if (chunkNum > numChunks) {
        throw new IIOException
            ("Image format Error; chunk num > num chunks");
      }

      // if there are no more chunks, set up the data
      if (numChunks == 1) {
        // reduce the stored length by the two chunk numbering bytes
        length -= 2;
        profile = new byte[length];
        buffer.bufPtr += 2;
        buffer.bufAvail -= 2;
        buffer.readData(profile);
        inICC = false;
      } else {
        // If we store them away, include the chunk numbering bytes
        byte[] profileData = new byte[length];
        // Now reduce the stored length by the
        // two chunk numbering bytes
        length -= 2;
        buffer.readData(profileData);
        chunks = new ArrayList();
        chunks.add(profileData);
        chunksRead = 1;
        inICC = true;
      }
    }

    ICCMarkerSegment(Node node) throws IIOInvalidTreeException {
      super(JPEG.APP2);
      if (node instanceof IIOMetadataNode) {
        IIOMetadataNode ourNode = (IIOMetadataNode) node;
        ICC_Profile prof = (ICC_Profile) ourNode.getUserObject();
        if (prof != null) {  // May be null
          profile = prof.getData();
        }
      }
    }

    protected Object clone() {
      ICCMarkerSegment newGuy = (ICCMarkerSegment) super.clone();
      if (profile != null) {
        newGuy.profile = (byte[]) profile.clone();
      }
      return newGuy;
    }

    boolean addData(JPEGBuffer buffer) throws IOException {
      if (debug) {
        System.out.println("Adding to ICC segment");
      }
      // skip the tag
      buffer.bufPtr++;
      buffer.bufAvail--;
      // Get the length, but not in length
      int dataLen = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
      dataLen |= buffer.buf[buffer.bufPtr++] & 0xff;
      buffer.bufAvail -= 2;
      // Don't include length itself
      dataLen -= 2;
      // skip the id
      buffer.bufPtr += ID_SIZE; // Skip the id
      buffer.bufAvail -= ID_SIZE;
            /*
             * Reduce the stored length by the id size.  The stored
             * length is used to store the length of the profile
             * data only.
             */
      dataLen -= ID_SIZE;

      // get the chunk number
      int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
      if (chunkNum > numChunks) {
        throw new IIOException
            ("Image format Error; chunk num > num chunks");
      }

      // get the number of chunks, which should match
      int newNumChunks = buffer.buf[buffer.bufPtr + 1] & 0xff;
      if (numChunks != newNumChunks) {
        throw new IIOException
            ("Image format Error; icc num chunks mismatch");
      }
      dataLen -= 2;
      if (debug) {
        System.out.println("chunkNum: " + chunkNum
            + ", numChunks: " + numChunks
            + ", dataLen: " + dataLen);
      }
      boolean retval = false;
      byte[] profileData = new byte[dataLen];
      buffer.readData(profileData);
      chunks.add(profileData);
      length += dataLen;
      chunksRead++;
      if (chunksRead < numChunks) {
        inICC = true;
      } else {
        if (debug) {
          System.out.println("Completing profile; total length is "
              + length);
        }
        // create an array for the whole thing
        profile = new byte[length];
        // copy the existing chunks, releasing them
        // Note that they may be out of order

        int index = 0;
        for (int i = 1; i <= numChunks; i++) {
          boolean foundIt = false;
          for (int chunk = 0; chunk < chunks.size(); chunk++) {
            byte[] chunkData = (byte[]) chunks.get(chunk);
            if (chunkData[0] == i) { // Right one
              System.arraycopy(chunkData, 2,
                  profile, index,
                  chunkData.length - 2);
              index += chunkData.length - 2;
              foundIt = true;
            }
          }
          if (foundIt == false) {
            throw new IIOException
                ("Image Format Error: Missing ICC chunk num " + i);
          }
        }

        chunks = null;
        chunksRead = 0;
        numChunks = 0;
        inICC = false;
        retval = true;
      }
      return retval;
    }

    IIOMetadataNode getNativeNode() {
      IIOMetadataNode node = new IIOMetadataNode("app2ICC");
      if (profile != null) {
        node.setUserObject(ICC_Profile.getInstance(profile));
      }
      return node;
    }

    /**
     * No-op.  Profiles are never written from metadata.
     * They are written from the ColorSpace of the image.
     */
    void write(ImageOutputStream ios) throws IOException {
      // No-op
    }

    void print() {
      printTag("ICC Profile APP2");
    }
  }
}
